Informe análisis de datos German Credit Risk#

Análisis Exploratorio de Datos German Credit Risk#

import psycopg2
import pandas as pd 
import kaleido
from sqlalchemy import create_engine
import numpy as np
import plotly.graph_objects as go
import sklearn.metrics
from sklearn.metrics import auc
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import train_test_split
from scipy.stats import kstest
import statsmodels.api as sm
import statsmodels.graphics.tsaplots as tsaplots
import scipy.stats as stats

from sklearn.model_selection import train_test_split, GridSearchCV, KFold,validation_curve

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler,LabelEncoder,RobustScaler

from sklearn.compose import ColumnTransformer
from sklearn.neighbors import KNeighborsClassifier

from sklearn.metrics import classification_report, ConfusionMatrixDisplay,confusion_matrix
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

import matplotlib.pyplot as plt
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 13
     11 from sklearn.model_selection import train_test_split
     12 from scipy.stats import kstest
---> 13 import statsmodels.api as sm
     14 import statsmodels.graphics.tsaplots as tsaplots
     15 import scipy.stats as stats

ModuleNotFoundError: No module named 'statsmodels'
connection =  psycopg2.connect(database="pfinal_db", user="pfinal", password="password", host="127.0.0.1", port="5432")
cursor = connection.cursor()

Con el fragmento de código relacionado a continuación se crea tabla german_data_risk

cursor.execute('''DROP TABLE IF EXISTS german_data_risk''')
cursor.execute('''CREATE TABLE german_data_risk(
    Status_of_existing_checking_account  VARCHAR(3),
    Duration_in_month INT,
    Credit_history VARCHAR(3),
    Purpose VARCHAR(3), 
    Credit_amount INT, 
    Savings_account_bonds VARCHAR(3), 
    Present_employment_since VARCHAR(3), 
    Installment_rate_in_percentage_of_disposable_income INT, 
    Personal_statu_and_sex VARCHAR(4), 
    Other_debtors_guarantors VARCHAR(4), 
    Present_residence_since INT, 
    Property VARCHAR(4),
    Age_in_years INT, 
    Other_installment_plans VARCHAR(4), 
    Housing VARCHAR(4),
    Number_of_existing_credits_at_this_bank INT, 
    Job VARCHAR(4), 
    Number_of_people_being_liable_to_provide_maintenance_for INT, 
    Telephone VARCHAR(4), 
    foreign_worker VARCHAR(4),
    Class VARCHAR(1));''')
connection.commit()
df = pd.read_csv("C:/Users/kathy/Desktop/Visualizacion_cientifica/P_final/datos/german_credit_risk.csv", sep=';')
df['Class'] = df['Class'].astype(int)
df.head()
Status_of_existing_checking_account Duration_in_month Credit_history Purpose Credit_amount Savings_account_bonds Present_employment_since Installment_rate_in_percentage_of_disposable_income Personal_statu_ and_sex Other_debtors_guarantors ... Property Age_in_years Other_installment_plans Housing Number_of_existing_credits_at_this_bank Job Number_of_ people_being_liable_to_provide_maintenance_for Telephone foreign_worker Class
0 A11 6 A34 A43 1169 A65 A75 4 A93 A101 ... A121 67 A143 A152 2 A173 1 A192 A201 1
1 A12 48 A32 A43 5951 A61 A73 2 A92 A101 ... A121 22 A143 A152 1 A173 1 A191 A201 2
2 A14 12 A34 A46 2096 A61 A74 2 A93 A101 ... A121 49 A143 A152 1 A172 2 A191 A201 1
3 A11 42 A32 A42 7882 A61 A74 2 A93 A103 ... A122 45 A143 A153 1 A173 2 A191 A201 1
4 A11 24 A33 A40 4870 A61 A73 3 A93 A101 ... A124 53 A143 A153 2 A173 2 A191 A201 2

5 rows × 21 columns

Con el siguiente fragmento de código creo la conexión usando la librería sqlalchemy para cargar en PostgreSQL el archivo CSV que contiene la información y/o datos de GERMAN CREDIT RISK.

db_params = {
    'host': 'localhost',
    'database': 'pfinal_db',
    'user': 'pfinal',
    'password': 'password'
}
engine = create_engine(f'postgresql://{db_params["user"]}:{db_params["password"]}@{db_params["host"]}/{db_params["database"]}')
df.to_sql('german_data_risk', engine, if_exists = 'replace', index=False, method='multi')
connection.commit()

Descripción de los datos German Credit Risk.#

A continuación, se describe cada una las variables de la tabla german_data_risk.

  • Variable 1: (cualitativa) Estado de Cuenta Corriente

    Código

    Descripción

    A11

    Más de 0 DM

    A12

    Entre 0 y 200 DM

    A13

    Más de 200 DM

    A14

    No tiene cuenta corriente

  • Variable 2: (numérica) Duración en meses.

  • Variable 3: (cualitativa) Historia crediticia

    Código

    Descripción

    A30

    No se han tomado créditos / Todos los créditos reembolsados debidamente

    A31

    Todos los créditos en este banco reembolsados debidamente

    A32

    Créditos existentes reembolsados debidamente hasta ahora

    A33

    Retraso en el pago en el pasado

    A34

    Cuenta crítica / Otros créditos existentes (no en este banco)

  • Variable 4: (cualitativa) Propósito

    Código

    Descripción

    A40

    Carro (nuevo)

    A41

    Carro (usado)

    A42

    Equipamiento / Amoblado

    A43

    Radio / Televisión

    A44

    Usos domésticos

    A45

    Reparaciones

    A46

    Educación

    A47

    Vacaciones

    A48

    Reciclaje

    A49

    Negocios

    A410

    Otros

  • Variable 5: (numérica) Monto de crédito.

  • Variable 6: (cualitativa) Estado de Cuenta de Ahorro

    Código

    Descripción

    A61

    Menos de 100 DM

    A62

    Entre 100 y 500 DM

    A63

    Entre 500 y 1000 DM

    A64

    Más de 1000 DM

    A65

    Desconocido / No tiene cuenta de ahorros

  • Variable 7: (cualitativa) Actualmente empleado desde

    Código

    Descripción

    A71

    Desempleado

    A72

    Menos de 1 año

    A73

    Entre 1 y 4 años

    A74

    Entre 4 y 7 años

    A75

    Más de 7 años

  • Variable 8: (numérica) Tasa de pago a plazos en porcentaje del ingreso disponible.

  • Variable 9: (cualitativa) Estado civil y género

    Código

    Descripción

    A91

    Masculino – Divorciado o separado

    A92

    Femenino – Divorciada, separada o casada

    A93

    Masculino – Soltero

    A94

    Masculino – Casado / viudo

    A95

    Femenino – Soltera

  • Variable 10: (cualitativa) Otros deudores / garantes

    Código

    Descripción

    A101

    Ninguno

    A102

    Co solicitante

    A103

    Garante

  • Variable 11: (numérica) Residencia actual desde.

  • Variable 12: (cualitativa) Propiedad

    Código

    Descripción

    A121

    Bienes raíces

    A122

    NO Bienes raíces / Acuerdo de ahorro de sociedad de construcción / Seguro de vida

    A123

    NO Bienes raíces / Carro u otro (no incluido en variable 6)

    A124

    Desconocido / Sin propiedad

  • Variable 13: (numérica) Edad en años.

  • Variable 14: (cualitativa) Otros planes de pago

    Código

    Descripción

    A141

    Banco

    A142

    Tiendas

    A143

    Ninguno

  • Variable 15: (cualitativa) Tipo de Vivienda

    Código

    Descripción

    A151

    Rentado

    A152

    Propio

    A153

    Gratis

  • Variable 16: (numérica) Número de créditos existentes con este banco

  • Variable 17: (cualitativa) Estado Trabajo

    Código

    Descripción

    A171

    Desempleado / No calificado y no residente

    A172

    No calificado y residente

    A173

    Empleado calificado / Oficial

    A174

    Gestión/ Autónomo / Empleado / Funcionario altamente calificado

  • Variable 18: (numérica) Número de personas responsables de proporcionar mantenimiento.

  • Variable 19: (cualitativa) Resgistra Teléfono

    Código

    Descripción

    A191

    Ninguno

    A192

    Si, registrado bajo el nombre del usuario

  • Variable 20: (cualitativa) Trabajador extranjero

    Código

    Descripción

    A201

    Si

    A202

    No

  • Matriz de costo:

    Este conjunto de datos requiere el uso de una matriz de costo, como se muestra a continuación:

    (1 = Bueno, 2 = Malo)

    1

    2

    1

    0

    1

    2

    5

    0

Las filas representan la clasificación real y las columnas la clasificación predicha. Es peor clasificar a un cliente como bueno cuando es malo (5), que clasificar a un cliente como malo cuando es bueno (1).

A continuación, presentamos la descripción general del conjunto de datos german_data_risk.

print("Tipo de datos:")
print(df.dtypes)

total_registros = df.shape[0]
Tipo de datos:
Status_of_existing_checking_account                          object
Duration_in_month                                             int64
Credit_history                                               object
Purpose                                                      object
Credit_amount                                                 int64
Savings_account_bonds                                        object
Present_employment_since                                     object
Installment_rate_in_percentage_of_disposable_income           int64
Personal_statu_ and_sex                                      object
Other_debtors_guarantors                                     object
 Present_residence_since                                      int64
Property                                                     object
Age_in_years                                                  int64
Other_installment_plans                                      object
Housing                                                      object
Number_of_existing_credits_at_this_bank                       int64
Job                                                          object
Number_of_ people_being_liable_to_provide_maintenance_for     int64
Telephone                                                    object
foreign_worker                                               object
Class                                                         int32
dtype: object
cursor.execute('SELECT count(1) FROM german_data_risk;')
record = cursor.fetchone()
print(record)
(1000,)

Hemos observado que nuestro conjunto de datos almacenado en pfinal_db consta de 21 variables (columnas) que tienen tipos de datos float64 y object. El tipo de dato float64 incluye valores numéricos con decimales, mientras que el tipo object abarca variables que contienen información textual o mixta (texto y números) en un formato no numérico (datos categóricos). En total, tenemos 1000 registros en nuestro conjunto de datos.

De igual forma se observan espacios entre los nombres de nuestras variables, por tal razon procedemos a reenombrarlas con el siguiente fragmento de código:

df_r = df.rename({'Status_of_existing_checking_account': 'c_corriente', 
               'Duration_in_month': 'mes', 
               'Credit_history': 'h_crediticia', 
               'Purpose': 'proposito', 
               'Credit_amount': 'monto', 
               'Savings_account_bonds': 'c_ahorro', 
               'Present_employment_since': 'empleado_desde',
               'Installment_rate_in_percentage_of_disposable_income': 'tasa_pago',
               'Personal_statu_ and_sex': 'ecivil_genero',
               'Other_debtors_guarantors': 'codeudores',
               ' Present_residence_since': 'residencia_desde',
               'Property': 'propiedades',
               'Age_in_years': 'edad',
               'Other_installment_plans': 'otros_pagos',
               'Housing': 'tipo_vivienda',
               'Number_of_existing_credits_at_this_bank': 'creditos_activos',
               'Job': 'trabajo',
               'Number_of_ people_being_liable_to_provide_maintenance_for': 'personas_a_cargo',
               'Telephone': 'tiene_telefono',
               'foreign_worker': 'extranjero',
               'Class': 'clasificacion',}, axis=1)
df_r.head()
c_corriente mes h_crediticia proposito monto c_ahorro empleado_desde tasa_pago ecivil_genero codeudores ... propiedades edad otros_pagos tipo_vivienda creditos_activos trabajo personas_a_cargo tiene_telefono extranjero clasificacion
0 A11 6 A34 A43 1169 A65 A75 4 A93 A101 ... A121 67 A143 A152 2 A173 1 A192 A201 1
1 A12 48 A32 A43 5951 A61 A73 2 A92 A101 ... A121 22 A143 A152 1 A173 1 A191 A201 2
2 A14 12 A34 A46 2096 A61 A74 2 A93 A101 ... A121 49 A143 A152 1 A172 2 A191 A201 1
3 A11 42 A32 A42 7882 A61 A74 2 A93 A103 ... A122 45 A143 A153 1 A173 2 A191 A201 1
4 A11 24 A33 A40 4870 A61 A73 3 A93 A101 ... A124 53 A143 A153 2 A173 2 A191 A201 2

5 rows × 21 columns

# Data cleaning: eliminar columnas no útiles
nonusefulcolumns = ["proposito", "monto", "ecivil_genero", "codeudores", "residencia_desde", "otros_pagos","creditos_activos", "trabajo", "personas_a_cargo", "tiene_telefono", "extranjero", "empleado_desde", "edad", "mes", "tasa_pago"]
df_cleaned = df_r.drop(columns=nonusefulcolumns,axis=0)
# Reemplazar '2' con una cadena vacía solo en la columna 'columna_especifica'
df_r['clasificacion'] = df_r['clasificacion'].replace(2, 0)
print(df_r)
    c_corriente  mes h_crediticia proposito  monto c_ahorro empleado_desde  \
0           A11    6          A34       A43   1169      A65            A75   
1           A12   48          A32       A43   5951      A61            A73   
2           A14   12          A34       A46   2096      A61            A74   
3           A11   42          A32       A42   7882      A61            A74   
4           A11   24          A33       A40   4870      A61            A73   
..          ...  ...          ...       ...    ...      ...            ...   
995         A14   12          A32       A42   1736      A61            A74   
996         A11   30          A32       A41   3857      A61            A73   
997         A14   12          A32       A43    804      A61            A75   
998         A11   45          A32       A43   1845      A61            A73   
999         A12   45          A34       A41   4576      A62            A71   

     tasa_pago ecivil_genero codeudores  ...  propiedades edad  otros_pagos  \
0            4           A93       A101  ...         A121   67         A143   
1            2           A92       A101  ...         A121   22         A143   
2            2           A93       A101  ...         A121   49         A143   
3            2           A93       A103  ...         A122   45         A143   
4            3           A93       A101  ...         A124   53         A143   
..         ...           ...        ...  ...          ...  ...          ...   
995          3           A92       A101  ...         A121   31         A143   
996          4           A91       A101  ...         A122   40         A143   
997          4           A93       A101  ...         A123   38         A143   
998          4           A93       A101  ...         A124   23         A143   
999          3           A93       A101  ...         A123   27         A143   

    tipo_vivienda creditos_activos  trabajo personas_a_cargo  tiene_telefono  \
0            A152                2     A173                1            A192   
1            A152                1     A173                1            A191   
2            A152                1     A172                2            A191   
3            A153                1     A173                2            A191   
4            A153                2     A173                2            A191   
..            ...              ...      ...              ...             ...   
995          A152                1     A172                1            A191   
996          A152                1     A174                1            A192   
997          A152                1     A173                1            A191   
998          A153                1     A173                1            A192   
999          A152                1     A173                1            A191   

    extranjero clasificacion  
0         A201             1  
1         A201             0  
2         A201             1  
3         A201             1  
4         A201             0  
..         ...           ...  
995       A201             1  
996       A201             1  
997       A201             1  
998       A201             0  
999       A201             1  

[1000 rows x 21 columns]
print("Tipo de datos:")
print(df_r.dtypes)

total_registros = df_r.shape[0]
Tipo de datos:
c_corriente         object
mes                  int64
h_crediticia        object
proposito           object
monto                int64
c_ahorro            object
empleado_desde      object
tasa_pago            int64
ecivil_genero       object
codeudores          object
residencia_desde     int64
propiedades         object
edad                 int64
otros_pagos         object
tipo_vivienda       object
creditos_activos     int64
trabajo             object
personas_a_cargo     int64
tiene_telefono      object
extranjero          object
clasificacion        int32
dtype: object

Resumen de los datos german_data_risk#

Variables númericas:#
df_r[[ "mes", "monto", "tasa_pago", "residencia_desde", "edad", 
      "creditos_activos", "personas_a_cargo"]].describe()
mes monto tasa_pago residencia_desde edad creditos_activos personas_a_cargo
count 1000.000000 1000.000000 1000.000000 1000.000000 1000.000000 1000.000000 1000.000000
mean 20.903000 3271.258000 2.973000 2.845000 35.546000 1.407000 1.155000
std 12.058814 2822.736876 1.118715 1.103718 11.375469 0.577654 0.362086
min 4.000000 250.000000 1.000000 1.000000 19.000000 1.000000 1.000000
25% 12.000000 1365.500000 2.000000 2.000000 27.000000 1.000000 1.000000
50% 18.000000 2319.500000 3.000000 3.000000 33.000000 1.000000 1.000000
75% 24.000000 3972.250000 4.000000 4.000000 42.000000 2.000000 1.000000
max 72.000000 18424.000000 4.000000 4.000000 75.000000 4.000000 2.000000

Basándonos en los resultados obtenidos, podemos inferir varios temas asociados con la distribución de los datos:

  • Montos de crédito: La distribución de los montos de crédito parece ser amplia, con un valor mínimo de 250 y un valor máximo de 18424. Esto indica que hay una variabilidad significativa en la cantidad de créditos otorgados.

  • Edad de los solicitantes: La edad de los solicitantes varía desde 19 hasta 75 años, con una media de aproximadamente 35 años. Esto sugiere que la población de solicitantes de crédito abarca una amplia gama de edades.

  • Tasa de pagos: La mayoría de los clientes parecen preferir tasas de pago entre 2 y 4, como lo indica el 25% en el primer cuartil, 50% en la mediana y 75% en el tercer cuartil.

  • Residencia desde: La mayoría de los clientes han estado residiendo en su ubicación actual durante al menos 2 años, como lo indican los cuartiles.

  • Número de créditos activos y personas a cargo: La mayoría de los clientes tienen uno o dos créditos activos y una o ninguna persona a cargo. Esto sugiere que la mayoría de los solicitantes no tienen una carga financiera extrema en términos de múltiples créditos activos o personas a cargo.

En general, estos resultados proporcionan una visión de la distribución y las características de la población de solicitantes de crédito, lo que puede ser útil para comprender mejor el perfil de los clientes y tomar decisiones informadas en la gestión de créditos.

Variables Categóricas:#
# Columnnas objeto de análisis 
columns_to_analyze = ["c_corriente", "h_crediticia", "proposito", 
                      "c_ahorro", "empleado_desde", "ecivil_genero", 
                      "codeudores", "propiedades", "otros_pagos", 
                      "tipo_vivienda", "trabajo", "tiene_telefono", 
                      "extranjero", "clasificacion"]

# Crear un DataFrame vacío para almacenar los resultados de value_counts de cada columna
summary_df = pd.DataFrame(columns=['Variable', 'Valor', 'Frecuencia'])

# Iterar sobre cada columna y obtener sus frecuencias de valor
for col in columns_to_analyze:
    col_value_counts = df_r[col].value_counts().reset_index()
    col_value_counts.columns = ['Valor', 'Frecuencia']
    col_value_counts['Variable'] = col
    summary_df = pd.concat([summary_df, col_value_counts], ignore_index=True)

# Mostrar el resumen completo
print(summary_df)
          Variable Valor Frecuencia
0      c_corriente   A14        394
1      c_corriente   A11        274
2      c_corriente   A12        269
3      c_corriente   A13         63
4     h_crediticia   A32        530
5     h_crediticia   A34        293
6     h_crediticia   A33         88
7     h_crediticia   A31         49
8     h_crediticia   A30         40
9        proposito   A43        280
10       proposito   A40        234
11       proposito   A42        181
12       proposito   A41        103
13       proposito   A49         97
14       proposito   A46         50
15       proposito   A45         22
16       proposito   A44         12
17       proposito  A410         12
18       proposito   A48          9
19        c_ahorro   A61        603
20        c_ahorro   A65        183
21        c_ahorro   A62        103
22        c_ahorro   A63         63
23        c_ahorro   A64         48
24  empleado_desde   A73        339
25  empleado_desde   A75        253
26  empleado_desde   A74        174
27  empleado_desde   A72        172
28  empleado_desde   A71         62
29   ecivil_genero   A93        548
30   ecivil_genero   A92        310
31   ecivil_genero   A94         92
32   ecivil_genero   A91         50
33      codeudores  A101        907
34      codeudores  A103         52
35      codeudores  A102         41
36     propiedades  A123        332
37     propiedades  A121        282
38     propiedades  A122        232
39     propiedades  A124        154
40     otros_pagos  A143        814
41     otros_pagos  A141        139
42     otros_pagos  A142         47
43   tipo_vivienda  A152        713
44   tipo_vivienda  A151        179
45   tipo_vivienda  A153        108
46         trabajo  A173        630
47         trabajo  A172        200
48         trabajo  A174        148
49         trabajo  A171         22
50  tiene_telefono  A191        596
51  tiene_telefono  A192        404
52      extranjero  A201        963
53      extranjero  A202         37
54   clasificacion     1        700
55   clasificacion     0        300

Procesamiento de Datos.#

Datos faltantes:#

A continuación, se realiza un análisis para identificar la presencia de datos faltantes en nuestro conjunto de datos. Para esto usamos el siguiente código:

pd.isna(df_r).sum()
c_corriente         0
mes                 0
h_crediticia        0
proposito           0
monto               0
c_ahorro            0
empleado_desde      0
tasa_pago           0
ecivil_genero       0
codeudores          0
residencia_desde    0
propiedades         0
edad                0
otros_pagos         0
tipo_vivienda       0
creditos_activos    0
trabajo             0
personas_a_cargo    0
tiene_telefono      0
extranjero          0
clasificacion       0
dtype: int64

Detección y manejo de valores atípicos (outliers):#

A continuación, se realiza un análisis para identificar la presencia de outliers en nuestro conjunto de datos. Para esto usamos el método de detección de valores atípicos mediante las puntuaciones \( Z \), el cual establece el siguiente criterio: Cualquier dato cuya puntuación esté fuera de la tercera desviación estándar es un valor atípico.

import numpy as np

#Definición de función para detección de datos atípicos a través de ZScore

def outliers_zscore(data):
    outliers = []    
    thres = 3
    mean = np.mean(data)
    std = np.std(data)
    for i in data:
        z_score = (i - mean)/std
        if (np.abs(z_score) > thres):
            outliers.append(i)
    return outliers
from IPython.display import display, Markdown

display(Markdown('<center><img src="img/outlierszscores.png" alt="figure"></center>'))
figure

La función outliers_zscore recorrer todos los datos y calcular la puntuación \( Z \) para cada punto de datos utilizando la fórmula \( \frac{(x_i - \mu)}{\sigma} \), donde \( x_i \) es cada valor de los datos, \( \mu \) es la media de los datos y \( \sigma \) es la desviación estándar de los datos. Luego, establece un umbral en la tercera desviación estandar e identifica como valores atípicos aquellos datos cuya puntuación \( Z \) (en valor absoluto) excede este umbral.

A continuación, se observan los datos atípicos obtenidos luego de aplicar el método descrito anteriormente.

df_n= df_r[[ "mes", "monto", "tasa_pago", "residencia_desde", "edad", 
      "creditos_activos", "personas_a_cargo"]]

mes_outliers = outliers_zscore(df_n.mes)
monto_outliers = outliers_zscore(df_n.monto)
tasa_pago_outliers = outliers_zscore(df_n.tasa_pago)
residencia_desde_outliers = outliers_zscore(df_n.residencia_desde)
edad_outliers = outliers_zscore(df_n.edad)
creditos_activos_outliers = outliers_zscore(df_n.creditos_activos)
personas_a_cargo_outliers = outliers_zscore(df_n.personas_a_cargo)

print("Outliers a través Método Z-scores para la variable Duración en meses: ", mes_outliers)
print("Outliers a través Método Z-scores para la variable Monto de crédito: ", monto_outliers)
print("Outliers a través Método Z-scores para la variable Tasa de pago a plazos en porcentaje del ingreso disponible.: ", tasa_pago_outliers)
print("Outliers a través Método Z-scores para la variable Residencia actual desde: ", residencia_desde_outliers)
print("Outliers a través Método Z-scores para la variable Edad en años: ", edad_outliers)
print("Outliers a través Método Z-scores para la variable Número de créditos existentes con este banco: ", creditos_activos_outliers)
print("Outliers a través Método Z-scores para la variable Número de personas a cargo: ", personas_a_cargo_outliers)
Outliers a través Método Z-scores para la variable Duración en meses:  [60, 60, 60, 60, 60, 60, 60, 60, 60, 72, 60, 60, 60, 60]
Outliers a través Método Z-scores para la variable Monto de crédito:  [12579, 14421, 12612, 15945, 11938, 14555, 12169, 11998, 13756, 14782, 14318, 12976, 11760, 12389, 12204, 15653, 14027, 14179, 12680, 15857, 11816, 15672, 18424, 14896, 12749]
Outliers a través Método Z-scores para la variable Tasa de pago a plazos en porcentaje del ingreso disponible.:  []
Outliers a través Método Z-scores para la variable Residencia actual desde:  []
Outliers a través Método Z-scores para la variable Edad en años:  [70, 74, 75, 74, 75, 74, 74]
Outliers a través Método Z-scores para la variable Número de créditos existentes con este banco:  [4, 4, 4, 4, 4, 4]
Outliers a través Método Z-scores para la variable Número de personas a cargo:  []

Análisis Univariado#

Variables numéricas:#

A continuación, se presenta el fragmento de código correspondiente a la función que generá dos tipos diferentes de gráficos (Histograma y Diagrama de Cajas y Bigotes) para visualizar una variable unidimensional.

import plotly.figure_factory as ff
import pandas as pd

def double_plot(data, columna, descripcion):
    fig = go.Figure()
    fig = ff.create_distplot([data[columna].to_list()],[descripcion],show_rug=False)

    fig.show()

A continuación se observa la distribución de nuestras variables objeto de análisis.

  • Duración en meses

df_r_b = df_n.copy()
bar_mes = double_plot(df_r_b,'mes','Mes')

En este histograma, podemos observar una distribución multimodal, indicada por la presencia de múltiples picos. Además, podemos notar un sesgo hacia la derecha, lo que sugiere la posible presencia de datos atípicos en el extremo superior de la distribución. La amplitud de la distribución también indica una variabilidad considerable en los datos.

  • Monto de crédito

df_r_b = df_n.copy()
bar_mes = double_plot(df_r_b,'monto','Monto')

En este histograma, podemos observar una distribución multimodal, indicada por la presencia de múltiples picos. Además, podemos notar un sesgo hacia la derecha, lo que sugiere la posible presencia de datos atípicos en el extremo superior de la distribución. La amplitud de la distribución también indica una variabilidad considerable en los datos.

  • Tasa de pago a plazos en porcentaje del ingreso disponible

df_r_b = df_n.copy()
bar_mes = double_plot(df_r_b,'tasa_pago','Tasa de Pago conforme a la capacidad de endeudamiento')

A pesar de tener una cantidad limitada de datos en el eje de las x, la gráfica exhibe una notable variabilidad en los datos, lo que sugiere una distribución multimodal en lugar de una distribución normal.

  • Residencia actual desde:

df_r_b = df_n.copy()
bar_mes = double_plot(df_r_b,'residencia_desde','Residencia actual desde')
  • Edad

df_r_b = df_n.copy()
bar_mes = double_plot(df_r_b,'edad','Edad')

La gráfica revela una distribución sesgada hacia la derecha, lo que indica que la mayoría de los clientes se concentran en el rango de edades entre 25 y 32 años. Además, se observa una tendencia consistente en la concesión de créditos, siendo este grupo de edad el receptor del mayor número de créditos. Este sesgo sugiere una preferencia o una mayor necesidad de servicios financieros en esta franja de edad específica.

  • Número de créditos existentes con este banco

df_r_b = df_n.copy()
bar_mes = double_plot(df_r_b,'creditos_activos','Número de créditos')

La gráfica indica que la mayoría de los clientes poseen un crédito existente con el banco, aunque el rango típico oscila entre 1 y 3 créditos. Sin embargo, los datos no siguen una distribución normal, lo que sugiere una variabilidad en el número de créditos mantenidos por los clientes, con una tendencia hacia la acumulación de un solo crédito.

  • Número de personas a cargo

df_r_b = df_n.copy()
bar_mes = double_plot(df_r_b,'personas_a_cargo','Número de Personas a Cargo')

La gráfica revela que la mayoría de los clientes tienen entre 1 y 2 personas a su cargo, siendo más frecuente aquellos con 1 persona a cargo.

De forma conjunta se pueden visualizar la distribución de cada una de nuestras variables númericas a través del siguiente gráfico de violín.

import matplotlib.pyplot as plt
import seaborn as sns

df_copy = df_r.copy()
fig, axes = plt.subplots(nrows=1, ncols=7, figsize=(20, 5))

sns.violinplot(data = df_copy, y= 'monto', ax=axes[0], color='lightblue')
axes[0].set(xlabel=None, ylabel= 'Monto')
sns.violinplot(data = df_copy, y= 'mes', ax=axes[1], color='#BC8F8F')
axes[1].set(xlabel=None, ylabel= 'Mes')
sns.violinplot(data = df_copy, y= 'tasa_pago', ax=axes[2], color='#DDA0DD')
axes[2].set(xlabel=None, ylabel= 'Tasa de pago')
sns.violinplot(data = df_copy, y= 'edad', ax=axes[3], color='#DB7093')
axes[3].set(xlabel=None, ylabel= 'Edad en años')
sns.violinplot(data = df_copy, y= 'creditos_activos', ax=axes[4], color='#B32781')
axes[4].set(xlabel=None, ylabel= 'Créditos activos')
sns.violinplot(data = df_copy, y= 'personas_a_cargo', ax=axes[5], color='#5012B3')
axes[5].set(xlabel=None, ylabel= 'Personas que tiene a cargo')
sns.violinplot(data = df_copy, y= 'residencia_desde', ax=axes[6], color='#410085')
axes[6].set(xlabel=None, ylabel= 'Residente desde (en meses)')
[Text(0.5, 0, ''), Text(0, 0.5, 'Residente desde (en meses)')]
_images/713a5d48a1f4f703f1079d24ed959a7eb3ebe9bd76f26994e84b0c19eb0434d2.png

Variables Categóricas#

A continuación, se presenta la distribución de cada una de las variables categóricas de nuestro conjunto de datos.

df_copy1 = df_r.copy()
fig, axes = plt.subplots(nrows=5, ncols=3, figsize=(18, 22))

sns.histplot(x = df_copy1['c_corriente'], kde= True, ax = axes[0,0],  color = '#BC8F8F', stat = 'density')
axes[0,0].set(ylabel = None, xlabel = 'Propósito del crédito')

sns.histplot(x = df_copy1['proposito'], kde= True, ax = axes[0,1],  color = '#DDA0DD', stat = 'density')
axes[0,1].set(ylabel = None, xlabel = 'Propósito del crédito')

sns.histplot(x = df_copy1['h_crediticia'], kde= True, ax = axes[0,2],  color = '#DB7093', stat = 'density')
axes[0,2].set(ylabel = None, xlabel = 'Historial crediticio')

#fila 2
sns.histplot(x = df_copy1['c_ahorro'], kde= True, ax = axes[1,0],  color = '#BC8F8F', stat = 'density')
axes[1,0].set(ylabel = None, xlabel = 'Cuentas de ahorro o bonos')

sns.histplot(x = df_copy1['empleado_desde'], kde= True, ax = axes[1,1],  color = '#DDA0DD', stat = 'density')
axes[1,1].set(ylabel = None, xlabel = 'Antigüedad en empleo')

sns.histplot(x = df_copy1['ecivil_genero'], kde= True, ax = axes[1,2],  color = '#DB7093', stat = 'density')
axes[1,2].set(ylabel = None, xlabel = 'Estado civil')

#fila 3
sns.histplot(x = df_copy1['codeudores'], kde= True, ax = axes[2,0],  color = '#BC8F8F', stat = 'density')
axes[2,0].set(ylabel = None, xlabel = 'Codedudores')

sns.histplot(x = df_copy1['propiedades'], kde= True, ax = axes[2,1],  color = '#DDA0DD', stat = 'density')
axes[2,1].set(ylabel = None, xlabel = 'Propiedades')

sns.histplot(x = df_copy1['otros_pagos'], kde= True, ax = axes[2,2],  color = '#DB7093', stat = 'density')
axes[2,2].set(ylabel = None, xlabel = 'Otros pagos')

#fila 4
sns.histplot(x = df_copy1['tipo_vivienda'], kde= True, ax = axes[3,0],  color = '#BC8F8F', stat = 'density')
axes[3,0].set(ylabel = None, xlabel = 'Tipo de vivienda')

sns.histplot(x = df_copy1['tiene_telefono'], kde= True, ax = axes[3,1],  color = '#DDA0DD', stat = 'density')
axes[3,1].set(ylabel = None, xlabel = 'Registro de teléfono')

sns.histplot(x = df_copy1['trabajo'], kde= True, ax = axes[3,2],  color = '#DB7093', stat = 'density')
axes[3,2].set(ylabel = None, xlabel = 'Trabajo')

#fila 5
sns.histplot(x = df_copy1['extranjero'], kde= True, ax = axes[4,0],  color = '#BC8F8F', stat = 'density')
axes[4,0].set(ylabel = None, xlabel = 'Extranjería del cliente')

sns.histplot(x = df_copy1['clasificacion'], kde= True, ax = axes[4,1],  color = '#BC8F8F', stat = 'density')
axes[4,1].set(ylabel = None, xlabel = 'Classificación')
[Text(0, 0.5, ''), Text(0.5, 0, 'Classificación')]
_images/4126f7767ad769f57abd8bdb59535ebb969cf04ba4c9b3631abbae8ce5d9e017.png

A continuación se graficara el comportamiento de las variable categórica con base a la clasificación de los clientes, para esto definimos la función figura_v_categoricas, la cual recibe como parámetros de entrada la columnas que no deseo gráficar, la categória que deseo gráficar y el color para cada una de las clasificaciones ( 1 - Buen Pagador, 0-Mal Pagador).

c_colors0 = ["#8B4513","#DEB887", "#FFEBCD","#C0C0C0", "#B0E0E6","#4682B4","#008080", "#B22222"]
c_colors1 = ["#8B4513","#DEB887", "#FFEBCD","#C0C0C0", "#B0E0E6","#4682B4","#008080", "#B22222"]

def figura_v_categoricas(columnas,categoria, color1, color0):
       
    dfb = df_cleaned.copy()
    
    
    #dfb = dfb[dfb['h_crediticia'].isin(credic_history_eda)]
    dfb.drop(columns=columnas,inplace=True,axis=0)
    dfb['clasificacion'] = dfb['clasificacion'].astype(int)
    dfb[categoria] = dfb[categoria].astype(str)
    
    dfb=pd.DataFrame(dfb.value_counts().reset_index(name='conteo'))
    

    figbar_c_history = go.Figure()

    temporal1 = dfb[dfb['clasificacion']==1]
    temporal2 = dfb[dfb['clasificacion']==0]
    
    figbar_c_history.add_trace(go.Bar(
        x=temporal1[categoria],
        y=temporal1['conteo'],
        name='Buen pagador',
        marker_color= color1,
        opacity=1, 
        hovertemplate=None
    ))

    figbar_c_history.add_trace(go.Bar(
        x=temporal2[categoria],
        y=temporal2['conteo'],
        name='Mal pagador',
        marker_color= color0,
        opacity=1,
        hovertemplate=None
    ))
    
    figbar_c_history.update_layout(
        xaxis_title="Clasificación",
        yaxis_title="Frecuencia",
        template="plotly_dark",
        plot_bgcolor="rgba(0, 0, 0, 0)",
        paper_bgcolor="rgba(0, 0, 0, 0)",
        font_color="gray",
        font_size=15,
        hovermode="x unified"
    )
    return figbar_c_history
  • Gráfico de Historia Crediticia vs Clasificación de clientes.

fig = figura_v_categoricas(['c_ahorro','c_corriente','propiedades','tipo_vivienda'],'h_crediticia', c_colors0[1], c_colors1[0])
fig.show()

A través de la gráfica podemos ver la mayoria de clientes a la fecha han cumplido con sus obligaciones de pago hasta el momento de la recoleccion de los datos (A32), y los clientes clasificados como buenos pagadores superan considerablemente a los clientes clasificados como mal pagadores. Otro patrón similar se da para el caso de los clientes que tienen créditos en otros bancos (A34). Para el caso de los clientes que se retrasan en los pagos (A33) fueron clasificados como buenos pagadores y duplican a los clasificados como malos pagadores.

  • Gráfico de Estado de Cuenta Corriente vs Clasificación de clientes.

fig = figura_v_categoricas(['c_ahorro','h_crediticia','propiedades','tipo_vivienda'],'c_corriente', c_colors0[4], c_colors1[5])
fig.show()

La gráfica revela que la mayoría de los clientes sin cuenta corriente (A14) son considerados buenos pagadores. Por otro lado, aquellos clientes que poseen cuenta corriente con la entidad financiera y tienen un saldo superior a 200 DM (A13) tienen un mayor número de buenos pagadores, aunque en comparación con los clientes con estados de cuenta diferentes, esta proporción es mínima

  • Gráfico de Estado de Cuenta Ahorros vs Clasificación de clientes.

fig = figura_v_categoricas(['c_corriente','h_crediticia','propiedades','tipo_vivienda'],'c_ahorro', c_colors0[1], c_colors1[7])
fig.show()

La gráfica muestra que el 50% de los clientes posee una cuenta de ahorro (A61), de los cuales el 56.6% fueron clasificados como buenos pagadores. Por otro lado, el 18% no cuenta con una cuenta de ahorro (A65), y el 82.5% de estos clientes fueron clasificados como buenos pagadores. El 32% restante de los clientes tiene entre 100 y 1000 DM en su cuenta de ahorros, y el 77.61% de este grupo total de clientes son clasificados como buenos pagadores.

  • Gráfico de tipo de Propiedade vs Clasificación de clientes.

fig = figura_v_categoricas(['c_corriente','h_crediticia','c_ahorro','tipo_vivienda'],'propiedades', c_colors0[2], c_colors1[5])
fig.show()

A traves del gráfico podemos observar que para todas las categórias la proporcion de clientes clasificados como buenos pagadores fue siempre mayor en comparacion con la proporcon de los clientes clasificados como malos pagadores.

  • Gráfico de tipo de vivienda vs Clasificación de clientes.

fig = figura_v_categoricas(['c_corriente','h_crediticia','c_ahorro','propiedades'],'tipo_vivienda', c_colors0[6], c_colors1[3])
fig.show()

El gráfico revela que el 71% de los clientes residen en viviendas propias (A152), de los cuales 527 están clasificados como buenos pagadores. Por otro lado, el 17.9% de los clientes viven en viviendas alquiladas (A151), mientras que el 11.1% vive en viviendas sin costo. Para estos dos últimos grupos, los clientes clasificados como buenos pagadores siempre superan en número a los clasificados como malos pagadores.

Análsis de Correlación#

A continuación se presenta análisis de correlación entre variables para evaluar la relación lineal entre pares de variables dentro del conjunto de datos a través de gráfico de dispersión.

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

df_r_c = df_n.copy()

sns.pairplot(df_r_c)
plt.show()
_images/5ae023f17a8dfe9ab663e378437ca774fba13a7dc24ce3d1aff6656d8b83d606.png

Análisis de correlación a través de mapa de calor.

import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

df_r_c = df_n.copy()

plt.figure(figsize=(5,5))
corr=df_r_c[:].corr()

mask = np.triu(np.ones_like(corr, dtype=bool))

sns.heatmap(corr, cmap=sns.cubehelix_palette(as_cmap=True), mask=mask, square = True, annot = True)
plt.show()
_images/d4a864944988f48f25b9485e483544a5bd2423acb517ac35c1c136237b048fce.png

Mátriz de correlación entre variables númericas.

print(df_r_c.corr())
                       mes     monto  tasa_pago  residencia_desde      edad  \
mes               1.000000  0.624984   0.074749          0.034067 -0.036136   
monto             0.624984  1.000000  -0.271316          0.028926  0.032716   
tasa_pago         0.074749 -0.271316   1.000000          0.049302  0.058266   
residencia_desde  0.034067  0.028926   0.049302          1.000000  0.266419   
edad             -0.036136  0.032716   0.058266          0.266419  1.000000   
creditos_activos -0.011284  0.020795   0.021669          0.089625  0.149254   
personas_a_cargo -0.023834  0.017142  -0.071207          0.042643  0.118201   

                  creditos_activos  personas_a_cargo  
mes                      -0.011284         -0.023834  
monto                     0.020795          0.017142  
tasa_pago                 0.021669         -0.071207  
residencia_desde          0.089625          0.042643  
edad                      0.149254          0.118201  
creditos_activos          1.000000          0.109667  
personas_a_cargo          0.109667          1.000000  

A través del mapa de calor podemos observar gráfica y numéricamente, podemos observar que no hay una correlación significativa entre la mayoría de nuestras variables. En algunos casos, como la relación entre la edad y la residencia, la correlación es débil. Sin embargo, se identifica una correlación moderada entre el mes y la duración en meses de los créditos.

  • Análisis de asociación: Tablas cruzadas.

df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['c_corriente'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['c_corriente'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Estado de cuentas de cheque existentes y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Estado de cuentas de cheque existentes y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Estado de cuentas de cheque existentes y clasificacion:
clasificacion    0    1   All
c_corriente                  
A11            135  139   274
A12            105  164   269
A13             14   49    63
A14             46  348   394
All            300  700  1000

Tabla de Proporción entre Estado de cuentas de cheque existentes y clasificacion:
clasificacion         0         1
c_corriente                      
A11            0.492701  0.507299
A12            0.390335  0.609665
A13            0.222222  0.777778
A14            0.116751  0.883249
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['proposito'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['proposito'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Proposito y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Proposito y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Proposito y clasificacion:
clasificacion    0    1   All
proposito                    
A40             89  145   234
A41             17   86   103
A410             5    7    12
A42             58  123   181
A43             62  218   280
A44              4    8    12
A45              8   14    22
A46             22   28    50
A48              1    8     9
A49             34   63    97
All            300  700  1000

Tabla de Proporción entre Proposito y clasificacion:
clasificacion         0         1
proposito                        
A40            0.380342  0.619658
A41            0.165049  0.834951
A410           0.416667  0.583333
A42            0.320442  0.679558
A43            0.221429  0.778571
A44            0.333333  0.666667
A45            0.363636  0.636364
A46            0.440000  0.560000
A48            0.111111  0.888889
A49            0.350515  0.649485
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['h_crediticia'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['h_crediticia'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Historia Crediticia y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Historia Crediticia y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Historia Crediticia y clasificacion:
clasificacion    0    1   All
h_crediticia                 
A30             25   15    40
A31             28   21    49
A32            169  361   530
A33             28   60    88
A34             50  243   293
All            300  700  1000

Tabla de Proporción entre Historia Crediticia y clasificacion:
clasificacion         0         1
h_crediticia                     
A30            0.625000  0.375000
A31            0.571429  0.428571
A32            0.318868  0.681132
A33            0.318182  0.681818
A34            0.170648  0.829352
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['c_ahorro'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['c_ahorro'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Cuentas de Ahorro/Bono y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Cuentas de Ahorro/Bono y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Cuentas de Ahorro/Bono y clasificacion:
clasificacion    0    1   All
c_ahorro                     
A61            217  386   603
A62             34   69   103
A63             11   52    63
A64              6   42    48
A65             32  151   183
All            300  700  1000

Tabla de Proporción entre Cuentas de Ahorro/Bono y clasificacion:
clasificacion         0         1
c_ahorro                         
A61            0.359867  0.640133
A62            0.330097  0.669903
A63            0.174603  0.825397
A64            0.125000  0.875000
A65            0.174863  0.825137
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['empleado_desde'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['empleado_desde'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Empleado desde y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Empleado desde y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Empleado desde y clasificacion:
clasificacion     0    1   All
empleado_desde                
A71              23   39    62
A72              70  102   172
A73             104  235   339
A74              39  135   174
A75              64  189   253
All             300  700  1000

Tabla de Proporción entre Empleado desde y clasificacion:
clasificacion          0         1
empleado_desde                    
A71             0.370968  0.629032
A72             0.406977  0.593023
A73             0.306785  0.693215
A74             0.224138  0.775862
A75             0.252964  0.747036
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['ecivil_genero'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['ecivil_genero'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Estado Civil - Género  y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Estado Civil - Género y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Estado Civil - Género  y clasificacion:
clasificacion    0    1   All
ecivil_genero                
A91             20   30    50
A92            109  201   310
A93            146  402   548
A94             25   67    92
All            300  700  1000

Tabla de Proporción entre Estado Civil - Género y clasificacion:
clasificacion         0         1
ecivil_genero                    
A91            0.400000  0.600000
A92            0.351613  0.648387
A93            0.266423  0.733577
A94            0.271739  0.728261
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['codeudores'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['codeudores'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Otros Deudores - Garantes y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Otros Deudores - Garantes y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Otros Deudores - Garantes y clasificacion:
clasificacion    0    1   All
codeudores                   
A101           272  635   907
A102            18   23    41
A103            10   42    52
All            300  700  1000

Tabla de Proporción entre Otros Deudores - Garantes y clasificacion:
clasificacion         0         1
codeudores                       
A101           0.299890  0.700110
A102           0.439024  0.560976
A103           0.192308  0.807692
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['propiedades'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['propiedades'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Propiedad y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Propiedad y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Propiedad y clasificacion:
clasificacion    0    1   All
propiedades                  
A121            60  222   282
A122            71  161   232
A123           102  230   332
A124            67   87   154
All            300  700  1000

Tabla de Proporción entre Propiedad y clasificacion:
clasificacion         0         1
propiedades                      
A121           0.212766  0.787234
A122           0.306034  0.693966
A123           0.307229  0.692771
A124           0.435065  0.564935
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['otros_pagos'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['otros_pagos'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Otros Planes de pago y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Otros Planes de pago  y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Otros Planes de pago y clasificacion:
clasificacion    0    1   All
otros_pagos                  
A141            57   82   139
A142            19   28    47
A143           224  590   814
All            300  700  1000

Tabla de Proporción entre Otros Planes de pago  y clasificacion:
clasificacion         0         1
otros_pagos                      
A141           0.410072  0.589928
A142           0.404255  0.595745
A143           0.275184  0.724816
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['tipo_vivienda'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['tipo_vivienda'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Tipo de vivienda y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Tipo de vivienda y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Tipo de vivienda y clasificacion:
clasificacion    0    1   All
tipo_vivienda                
A151            70  109   179
A152           186  527   713
A153            44   64   108
All            300  700  1000

Tabla de Proporción entre Tipo de vivienda y clasificacion:
clasificacion         0         1
tipo_vivienda                    
A151           0.391061  0.608939
A152           0.260870  0.739130
A153           0.407407  0.592593
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['tiene_telefono'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['tiene_telefono'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Teléfono y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Historia Teléfono y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Teléfono y clasificacion:
clasificacion     0    1   All
tiene_telefono                
A191            187  409   596
A192            113  291   404
All             300  700  1000

Tabla de Proporción entre Historia Teléfono y clasificacion:
clasificacion          0         1
tiene_telefono                    
A191            0.313758  0.686242
A192            0.279703  0.720297
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['trabajo'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['trabajo'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Trabajo y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Trabajo y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Trabajo y clasificacion:
clasificacion    0    1   All
trabajo                      
A171             7   15    22
A172            56  144   200
A173           186  444   630
A174            51   97   148
All            300  700  1000

Tabla de Proporción entre Trabajo y clasificacion:
clasificacion         0         1
trabajo                          
A171           0.318182  0.681818
A172           0.280000  0.720000
A173           0.295238  0.704762
A174           0.344595  0.655405
df_c= df_r[[ "c_corriente", "proposito", "h_crediticia", "c_ahorro", "empleado_desde", 
      "ecivil_genero", "codeudores", "propiedades", "otros_pagos", "tipo_vivienda", "tiene_telefono", "trabajo", 
      "extranjero", "clasificacion"]]


# Calcular tabla de frecuencias cruzadas con margen (margen en variable1)
tabla_cruzada_2 = pd.crosstab(df_c['extranjero'], df_c['clasificacion'], margins=True)

# Calcular tabla de frecuencias cruzadas con porcentajes por fila
tabla_cruzada_3 = pd.crosstab(df_c['extranjero'], df_c['clasificacion'], normalize='index')

print("\nTabla de Frecuencias Cruzadas entre Sí es recidente extranjero y clasificacion:")
print(tabla_cruzada_2)
print("\nTabla de Proporción entre Sí es recidente extranjero y clasificacion:")
print(tabla_cruzada_3)
Tabla de Frecuencias Cruzadas entre Sí es recidente extranjero y clasificacion:
clasificacion    0    1   All
extranjero                   
A201           296  667   963
A202             4   33    37
All            300  700  1000

Tabla de Proporción entre Sí es recidente extranjero y clasificacion:
clasificacion         0         1
extranjero                       
A201           0.307373  0.692627
A202           0.108108  0.891892

Resultado#

Aplicar un Análisis Exploratorio de Datos (EDA) sobre la base de datos German Credit Risk proporcionó una visión detallada y significativa de las características clave relacionadas con el riesgo crediticio. Durante el EDA, se examinaron minuciosamente variables como el historial crediticio, el propósito del crédito, la edad, el estado civil, el empleo y otros factores relevantes. Este proceso permitió identificar la distribución de estas variables, detectar posibles correlaciones entre ellas y comprender cómo influyen en las decisiones crediticias. Además, el EDA reveló patrones de comportamiento financiero y posibles anomalías o valores atípicos que se deben estudiar a profundidad.

Construcción de Modelos#

1 . Regresión Logística#

La regresión logísitica es un modelo que puede predecir la probabilidad que tiene una variable binaria (que puede aceptar 2 valores) de pertenecer a una clase o a otra. Es por tanto un método utilizado para la clasificación categórica de variables, especialmente útil por su simplicidad e interpretabilidad.

El modelo de regresión logística considera un conjunto de \(n\) observaciones indendientes de \(Y = (Y_1, Y_2, \ldots, Y_n)\) con los datos \(y_i \in \left\lbrace 0,1 \right\rbrace\) , para todo \(i=1,\ldots,n\) donde \(y_i\) es un posible valor de \(Y_i\), las cuales son independiente entre sí.

Dado lo anterior se llega a un modelo estadístico de Bernoulli:

\[ Y_i ~ B(1, p_i), \space i=1,\ldots n. \]

Fijando las \(y = (y_1, \dots, y_n)^T\) obtenemos la ecuación de verosimilitud en el parámetro \(p=(p_1, \dots,p_n)^T\) como se muestra a continuación:

\[ L(p)= \prod_{i=1}^n[p_i^{y_i}(1-p_i)^{1-y_i}] \]

De lo anterior obtenemos el logaritmo de la función de verosimilitud de la siguiente forma:

\[ \mathcal{L}(p):= ln\mathcal{L}(p) = \sum_{i=1}^n[y_ilnp_i + (1-y_i)ln(1-p_i)] \]

como \(0\leq f(y,p) \leq 1\), de la expresión anterior se tiene que:

\[ -\infty \leq \mathcal{L}(p) \leq 0 \]

A continuación se define la formula general para el modelo de regresión logístico:

\[ Logit(p_j) := ln\left(\frac{p_j}{1-p_j}\right) = \delta + \beta_1x_{ji} + \ldots + \beta_kx_{jk} \]

Donde \(\alpha = (\beta_1, \beta_2, \ldots, \beta_k)^T\) es el vector de parámetros en el modelo.

La ecuación para el riesgo \((p_j)\) estimado de este modelo esta dada bajo las siguientes condiciones:

Sea \(g_j:=\delta + \beta_1 x_{1j}+\beta_k x_{kj}\) , entonces la probabilidad \(p_j\) esta dada por \(p_j = P(Y_j = 1 \mid x_{1j}, \ldots , x{kj})\) de obtener un exito en la población \(j=1,2, \ldots, J\). Dado lo anterior la formula generar para estimar el riesgo es la que se muestra a continuación:

\[ p_j = logit^{-1}(g_j) = \frac{e^{g_i}}{1+e^{g_i}} \]

Modelo#

  • Preparación de datos: Dado que nuestro conjunto de datos incluye variables predictoras o explicativas categóricas con múltiples niveles, necesitamos realizar una transformación en nuestros datos originales utilizando el siguiente código. Esto es necesario ya que vamos a emplear la librería sklearn.

  • Las variables de interés para la construcción de nuestro modelo de clasificación se especifican como sigue: c_corriente, c_ahorro, h_crediticia, propiedades y tipo_vivienda. Estas variables se seleccionaron debido a su relevancia teórica y su potencial capacidad para discriminar entre las categorías de clasificación objetivo

# Data cleaning: eliminar columnas no útiles
nonusefulcolumns = ["proposito", "monto", "ecivil_genero", "codeudores", "residencia_desde", "otros_pagos","creditos_activos", "trabajo", "personas_a_cargo", "tiene_telefono", "extranjero", "empleado_desde", "edad", "mes", "tasa_pago"]
df_cleaned = df_r.drop(columns=nonusefulcolumns,axis=0)

#Crea una columna por cada nivel de nuestras variables predictoras de tipo categórico. 
datos_rl = df_cleaned.copy()
datos_rl = pd.get_dummies(datos_rl,dtype=int)
datos_rl.head()
clasificacion c_corriente_A11 c_corriente_A12 c_corriente_A13 c_corriente_A14 h_crediticia_A30 h_crediticia_A31 h_crediticia_A32 h_crediticia_A33 h_crediticia_A34 ... c_ahorro_A63 c_ahorro_A64 c_ahorro_A65 propiedades_A121 propiedades_A122 propiedades_A123 propiedades_A124 tipo_vivienda_A151 tipo_vivienda_A152 tipo_vivienda_A153
0 1 1 0 0 0 0 0 0 0 1 ... 0 0 1 1 0 0 0 0 1 0
1 0 0 1 0 0 0 0 1 0 0 ... 0 0 0 1 0 0 0 0 1 0
2 1 0 0 0 1 0 0 0 0 1 ... 0 0 0 1 0 0 0 0 1 0
3 1 1 0 0 0 0 0 1 0 0 ... 0 0 0 0 1 0 0 0 0 1
4 0 1 0 0 0 0 0 0 1 0 ... 0 0 0 0 0 0 1 0 0 1

5 rows × 22 columns

A continuación se configuran los parámetros de nuestro modelo de regresión:

  • Penalty: Especifica la norma utilizada en la penalización. En este caso, ‘l2’ indica que se utiliza la penalización L2, que es la norma Euclidiana al cuadrado.

  • Tol: Tolerancia para la detección de convergencia del algoritmo de optimización.

  • Class_weight: Peso asociado a las clases en el modelo. Si no se proporciona, todas las clases se asumen con el mismo peso.

  • Random_state: Semilla utilizada por el generador de números aleatorios para reproducibilidad.

  • Solver: Algoritmo a utilizar en el problema de optimización. En este caso, lbfgs indica que se utiliza el método de Broyden–Fletcher–Goldfarb–Shanno (L-BFGS).

  • Max_iter: Número máximo de iteraciones permitidas para la convergencia del algoritmo de optimización.

model1 = LogisticRegression(penalty='l2', tol=0.0001, random_state= 42, solver='lbfgs', max_iter=10000) #Definición de parámetros del modelo 1

A contnuación se ajusta el modelo a los datos de entrenamiento y se predicen las etiquetas de clase en el conjunto de prueba.

datosmod1 = datos_rl.copy() # Data cleaning: eliminar columnas no útiles
y = datosmod1['clasificacion'] # Extraer la variable dependiente (variable objetivo)
x = datosmod1.drop(columns=['clasificacion'])# Eliminar la columnas del DataFrame antes de aplicar One-Hot Encoding
x_train, x_test, y_train, y_test = train_test_split(x, y, random_state=42, test_size=0.30)# Dividir los datos en conjuntos de entrenamiento y prueba
mod1 = model1.fit(x_train, y_train) # Ajustar el modelo a los datos de entrenamiento
predictions1 = mod1.predict(x_test) # Predecir las etiquetas de clase en el conjunto de prueba

# Create lists
column_labels = x_train.columns.tolist() #Creo una lista con el nombre de las columnas
column_labels.append('Intercepto') #Adiciono a la lista la etiqueta intercepto. 


coef = mod1.coef_.squeeze().tolist() #Se extraen los coeficientes del modelo en formato lista

coef.append(mod1.intercept_[0]) #Se adiciona a la lista de coficientes del modelo el valor del intercepto que arroja el modelo. 

coef_model = pd.DataFrame([coef], columns=column_labels) #por ultimo convertimos la lista en un datafram indicando que la cabecera tome el nombre de column-labels
coef_model.head()
c_corriente_A11 c_corriente_A12 c_corriente_A13 c_corriente_A14 h_crediticia_A30 h_crediticia_A31 h_crediticia_A32 h_crediticia_A33 h_crediticia_A34 c_ahorro_A61 ... c_ahorro_A64 c_ahorro_A65 propiedades_A121 propiedades_A122 propiedades_A123 propiedades_A124 tipo_vivienda_A151 tipo_vivienda_A152 tipo_vivienda_A153 Intercepto
0 -0.729589 -0.397743 0.350128 0.779271 -0.646898 -0.489206 0.064946 0.188887 0.884338 -0.450038 ... 0.422071 0.04239 0.534272 0.038045 -0.041273 -0.528977 -0.208487 0.142817 0.067736 0.892932

1 rows × 22 columns

A continuación se describe la formula de nuestro modelo de regresión logístico, suponiendo que:

El cliente tiene un estado de cuenta corriente entre 0 y 200 DM(A11), No tiene cuenta de ahorros (A65), dentro de los créditos adquiridos con la entidad financiera todos han sido reembolsados debidamente (A31), cuenta con bienes raíces (A121) y cuenta con vivienda propia (A152)

\[ Logit(p_j) := 0.893 - 0.729x_{A11} + 0.0424x_{A65} - 0.489x_{A31} + 0.534_{A121} + 0.149x_{A152} \]

Test de Normalidad de los residuales a través de Kolmogorov-Smirnov#

  • Planteamiento de Hipótesis:

\[ H_0 : \text{Residuales se distribuyen normalmente}\]
\[ H_1 : \text{Residuales no se distribuyen normalmente}\]
  • Críterios de aceptación y/o rechazo:

Sí el P-value \(< 0.05 \), se rechaza la hipótesis nula con un a significancia \( \alpha = 0.05\), es decir que los datos no siguen una distribución normal.

Test de Independiencia de los residuales a través del Estadístico de Durbin Watson#

El estadístico de Durbin-Watson (DW) es una medida utilizada para evaluar la presencia de autocorrelación en los residuos de un modelo de regresión, éste toma valores entre 0 y 4. Un valor de DW cercano a 2 sugiere que no hay autocorrelación serial en los residuos. Esto significa que los residuos en un momento dado no están correlacionados con los residuos en momentos anteriores. Un valor de 2 implica la ausencia total de autocorrelación.Cuando el valor de DW se acerca a 0, indica autocorrelación positiva en los residuos. Esto significa que los residuos en un momento dado están correlacionados positivamente con los residuos en momentos anteriores. Por otro lado, si el valor de DW se acerca a 4, indica autocorrelación negativa en los residuos.

Análisis de residuales.#

# Calcular los residuos
ei_rl = y_test - predictions1

#Prueba de normalidad de Kolmogorov-Smirnov
from scipy.stats import kstest

ks_result = kstest(ei_rl, 'norm')
print("Prueba de Kolmogorov-Smirnov:")
print("Estadístico:", ks_result.statistic)
print("Valor p:", ks_result.pvalue)
Prueba de Kolmogorov-Smirnov:
Estadístico: 0.42666666666666664
Valor p: 4.364069962779605e-50

Dado que el P-value \(=4.3640e-50 < 0.05\) hay evidencia significativa para rechazar la hipótesis nula con una significancia \(\alpha = 0.05\), es decir que los residuos no siguen una distribución.normal

import statsmodels.api as sm

# Prueba de Durbin-Watson
durbin_watson_statistic = sm.stats.stattools.durbin_watson(ei_rl)
print("Estadístico de Durbin-Watson:", durbin_watson_statistic)
Estadístico de Durbin-Watson: 1.9113924050632911

Dado que el estadístico de Durbin_Watson \( = 1.911\), se puede afirmar que no existe una correlacion serial en los residuales.

import statsmodels.graphics.tsaplots as tsaplots


# Gráfico QQ-plot
fig, axs = plt.subplots(1, 1, figsize=(15, 5))

# QQ-plot para los residuales
stats.probplot(ei_rl, dist="norm", plot=axs)
axs.get_lines()[0].set_color('#008080')  # Establecer el color del gráfico
axs.get_lines()[0].set_markersize(2)     # Establecer el tamaño de los puntos
axs.set_title('QQ-plot para los residuales ordinarios', fontsize=16)
axs.tick_params(axis='x', labelsize=10)  # Solo para el eje x (inferior)
axs.tick_params(axis='y', labelsize=10)  # Solo para el eje y (izquierdo)

# Ajustar el espaciado entre los gráficos
plt.tight_layout()

# Mostrar la figura
plt.show()
_images/451d60fbabfb3f25962376b4be86194a4fd1c1f9d2c6650a9d0fb22af2410cba.png

Basándonos en el gráfico anterior, podemos concluir que, si bien los residuos no siguen estrictamente una distribución normal, sí se observa que cumplen con el supuesto de independencia. Esto se refiere a que los residuos no muestran patrones discernibles en su distribución, lo que sugiere que están aleatoriamente dispersos.

Métricas de evaluación de modelos#

Matriz de Confusión#

La matriz de confusión es una herramienta que permite visualizar el rendimiento de un modelo de clasificación al comparar las predicciones del modelo con las clases reales de los datos. Esta matriz organiza las predicciones en cuatro categorías: verdaderos positivos (TP), falsos positivos (FP), verdaderos negativos (TN) y falsos negativos (FN). Las entradas de la diagonal principal de la matriz de confusión corresponden a las clasificaciones correctas, mientras que las demás entradas nos indican cuántas muestras se han clasificado erróneamente en una clase.

from IPython.display import display, Markdown

display(Markdown('<center><img src="img/mconfusion.png" alt="figure"></center>'))
figure
Métricas para modelos de Clasificación#
  • Sensibilidad (Precision): proporción de predicciones verdaderas positivas con respecto al total de predicciones positivas. Permite detectar el error tipo I (Falsos Positivos).

\[ Precision = \frac{TP}{TP+FP}\]
  • Especificidad (Recall): proporción de predicciones verdaderas positivas con respecto al total de observaciones positivas. Permite detectar el error tipo II (Falsos Negativos).

\[ Recall = \frac{TP}{TP+FN}\]
  • F1 Score: El puntaje F1 es una medida que combina precision y recall en una sola métrica, proporcionando una evaluación global del rendimiento de un modelo de clasificación. Se calcula como la media armónica entre precision y recall.
    $\(F = 2\cdot \frac{precision\cdot recall}{precision + recall}\)$

  • ROC Curve (Receiver Operating Characteristic) : permite visualizar la habilidad de un modelo para distinguir entre dos clases al ajustar el umbral de decisión. Representa la sensibilidad (tasa de verdaderos positivos) frente a la especificidad (1 - tasa de falsos positivos). Cuanto más se acerque la curva al área bajo la curva (AUC) de 1, mejor será el modelo en diferenciar entre las clases. Es una herramienta crucial para evaluar y comparar modelos de clasificación binaria.

Función para estimación de Matriz de Confusión y Métricas para Modelos de Clasificación:

def metricas(x_test, y_test, y_hats, model): 
    mc = sklearn.metrics.confusion_matrix(y_test, y_hats)  # Calcula matriz de confusión
    precs = sklearn.metrics.precision_score(y_test, y_hats)  # Calcula Indicador de presición
    recall = sklearn.metrics.recall_score(y_test, y_hats)  # Calcula indicador Recall 
    f1score = sklearn.metrics.f1_score(y_test, y_hats)  # Calcula indicador F1 Score

   # Visualizar matriz de confusión cuando se ejecute la función
    vismc = sklearn.metrics.ConfusionMatrixDisplay(confusion_matrix=mc, display_labels=["Negativo", "Positivo"])  

    # Calculos para la visualizar la curva ROC
   
    y_prob_pred = model.predict_proba(x_test)[:, 1]  #Calcula la probabilidad de exito. 
    fpr, tpr, thresholds = sklearn.metrics.roc_curve(y_test, y_prob_pred) # Calcula los falsos positivos y falsos negativos para la curva ROC
    
    return mc, precs, recall, f1score, vismc, fpr, tpr, y_prob_pred

Indexación de parámetros para la función metricas()

La siguiente función evalúa el rendimiento del modelo de clasificación después de entrenarlo con el 70% de las observaciones reales. Las comparaciones se realizan utilizando el 30% restante de las observaciones, a partir de las cuales se derivan las métricas de Precision, Recall y F1 Score. Además de estas métricas, la función proporciona el gráfico de la matriz de confusión correspondiente al modelo, basado en el marco de la regresión logística binaria. También extrae otros parámetros necesarios para la creación de la curva ROC, como los falsos positivos y los verdaderos positivos.

mc1, precs1, recall1, f1score1, vismc1, fpr, tpr, y_prob_pred= metricas(x_test, y_test, predictions1, mod1)
vismc1.plot(cmap=plt.cm.RdPu)
print('La proporción de predicciones verdaderas positivas con respecto al total de predicciones positivas es de: ', precs1)
print('La proporción de predicciones verdaderas positivas con respecto al total de observaciones positivas es de:', recall1)
print('El puntaje F1 es de:', f1score1)
La proporción de predicciones verdaderas positivas con respecto al total de predicciones positivas es de:  0.7663934426229508
La proporción de predicciones verdaderas positivas con respecto al total de observaciones positivas es de: 0.8947368421052632
El puntaje F1 es de: 0.82560706401766
_images/a9c93f35a6d3e63e5c718a4c0ca32a611d136da7c7752ded75e9a308386e54f5.png

Según el análisis del gráfico, observamos el siguiente comportamiento del modelo de regresión logístico (Clasificación):

  • De un total de 300 predicciones realizadas, 187 se clasificaron correctamente como verdaderas positivas. Esto significa que se identificaron adecuadamente los clientes considerados como buenos pagadores, quienes en realidad lo son.

  • Asimismo, de las 300 predicciones, 34 fueron clasificadas correctamente como verdaderas negativas. Esto indica que se identificaron correctamente los clientes catalogados como malos pagadores, quienes realmente lo son.

  • Sin embargo, se presentaron 22 falsos negativos entre las predicciones. Esto implica que algunos clientes que en realidad son buenos pagadores fueron erróneamente clasificados como malos pagadores.

  • Por otro lado, se registraron 57 falsos positivos. Este resultado indica que algunos clientes que son malos pagadores fueron incorrectamente clasificados como buenos pagadores.

A continuación se visualiza la curva ROC.

# Calcular el AUC
auc_value = auc(fpr, tpr)

# Crear la figura
fig_roc1 = go.Figure()

# Agregar el área bajo la curva ROC
fig_roc1.add_trace(go.Scatter(
    x=fpr, y=tpr,
    fill='tozeroy',
    mode='lines',
    line=dict(color='rgba(128,0,128,0.2)'),
    name=f'ROC Curve (AUC={auc_value:.4f})'
))

# Agregar la línea de referencia diagonal
fig_roc1.add_shape(
    type='line', line=dict(dash='dash'),
    x0=0, x1=1, y0=0, y1=1
)

# Agregar etiqueta con el valor del AUC
fig_roc1.add_annotation(
    xref="paper", yref="paper",  # Coordenadas relativas al gráfico
    x=0.95, y=0.05,  # Coordenadas para la esquina inferior derecha
    text=f'AUC: {auc_value:.4f}',
    showarrow=False,
    font=dict(size=12, color='black'),  # Estilo del texto
    bordercolor='black',  # Color del borde del rectángulo
    borderwidth=1,  # Ancho del borde del rectángulo
    bgcolor='rgba(255,255,255,0.7)'  # Color de fondo del rectángulo con opacidad
)

# Actualizar las etiquetas y título
fig_roc1.update_layout(
    title='ROC Curve',
    xaxis_title='False Positive Rate',
    yaxis_title='True Positive Rate',
    width=700, height=500
)

fig_roc1.show()

Con un valor de 0,7741 para el área bajo la curva ROC (AUC), podemos afirmar que nuestro modelo tiene un buen rendimiento en términos de su capacidad para discriminar entre las clases positivas y negativas. Cuanto más cercano a 1 sea el valor de AUC, mejor será la capacidad predictiva del modelo, y un valor de 0,7741 indica una capacidad de discriminación sólida.

2. KNN (K vecinos más próximos)#

El algoritmo del vecino más cercano se destaca por su simplicidad dentro del campo del aprendizaje automático. Su enfoque se basa en memorizar el conjunto de entrenamiento y luego predecir la etiqueta del vecino más cercano en dicho conjunto. Este método se fundamenta en la premisa de que las características utilizadas para describir los puntos en el dominio son determinantes para sus etiquetas; por lo tanto, es probable que puntos cercanos compartan la misma etiqueta.

A diferencia de otros enfoques algorítmicos que requieren hipótesis predefinidas, el método del vecino más cercano calcula una etiqueta para cualquier punto de prueba sin necesidad de buscar un predictor dentro de una clase de funciones establecida de antemano.Lihki Rubio )

Modelo#

  • Las variables de interés para la construcción de nuestro modelo de clasificación se especifican como sigue: c_corriente, c_ahorro, h_crediticia, propiedades y tipo_vivienda. Estas variables se seleccionaron debido a su relevancia teórica y su potencial capacidad para discriminar entre las categorías de clasificación objetivo

# Data cleaning: eliminar columnas no útiles
nonusefulcolumns = ["proposito", "monto", "ecivil_genero", "codeudores", "residencia_desde", "otros_pagos","creditos_activos", "trabajo", "personas_a_cargo", "tiene_telefono", "extranjero", "empleado_desde", "edad", "mes", "tasa_pago"]
df_cleaned_knn = df_r.drop(columns=nonusefulcolumns,axis=0)
datosmod2 = df_cleaned_knn.copy() # Data cleaning: eliminar columnas no útiles
datosmod2 = pd.get_dummies(datosmod2,dtype=int)
print(datosmod2.dtypes)
clasificacion         int32
c_corriente_A11       int32
c_corriente_A12       int32
c_corriente_A13       int32
c_corriente_A14       int32
h_crediticia_A30      int32
h_crediticia_A31      int32
h_crediticia_A32      int32
h_crediticia_A33      int32
h_crediticia_A34      int32
c_ahorro_A61          int32
c_ahorro_A62          int32
c_ahorro_A63          int32
c_ahorro_A64          int32
c_ahorro_A65          int32
propiedades_A121      int32
propiedades_A122      int32
propiedades_A123      int32
propiedades_A124      int32
tipo_vivienda_A151    int32
tipo_vivienda_A152    int32
tipo_vivienda_A153    int32
dtype: object

A continuación realizamos la separación de nuestro conjunto de datos para obtener los datos de entrenamieno (70%) y los datos de test (30%).

train_knn, test_knn = train_test_split(datosmod2, test_size=0.3, random_state=77)

Luego de realizar el paso anterior procedemos a separar los datos.

x_train_knn = train_knn.drop(['clasificacion'],axis=1)
y_train_knn = train_knn['clasificacion']

x_test_knn = test_knn.drop(['clasificacion'],axis=1)
y_test_knn = test_knn['clasificacion']

El siguiente fragmento de código utiliza la biblioteca scikit-learn para crear un proceso de preprocesamiento de datos que escala las características numéricas utilizando StandardScaler.

Comienza importando StandardScaler de scikit-learn.preprocessing, un transformador que ajusta cada característica numérica para que tenga una media de 0 y una desviación estándar de 1. Luego, se importa ColumnTransformer de scikit-learn.compose, que permite aplicar transformaciones específicas a diferentes columnas de un conjunto de datos.

A continuación, se crea un objeto scaler de StandardScaler, el cual será utilizado para escalar las características numéricas. Posteriormente, se define un ColumnTransformer llamado transformer, que especifica que todas las características numéricas deben ser escaladas utilizando el scaler. Esto se logra mediante el argumento transformers, el cual toma una lista de tuplas, donde cada tupla contiene el nombre de la transformación y el transformador correspondiente. En este caso, el nombre es num y el transformador es scaler.

Finalmente, se define una lista llamada steps_knn, que contiene una tupla con el nombre scale y el transformador scaler. Esta lista se utilizará en un pipeline de scikit-learn para aplicar el escalado de características antes de entrenar un modelo, en este caso, de vecinos más cercanos (KNN).

En resumen, este código establece un proceso de preprocesamiento de datos que escala las características numéricas utilizando StandardScaler, como parte fundamental de la preparación de los datos para el modelo de aprendizaje automático.

scaler = StandardScaler()
transformer = ColumnTransformer(
    transformers=[
        ("num", scaler)
    ]
)
steps_knn = [
    ("scale", scaler)
]

Un pipeline en el contexto de aprendizaje automático es una secuencia de transformaciones de datos seguidas por la aplicación de un modelo predictivo. Esto permite que todo el proceso de preprocesamiento de datos y modelado se realice de manera ordenada y sistemática.

A continuación, se crea un pipeline de scikit-learn que puede ser usado para aplicar secuencialmente el preprocesamiento de datos y luego entrenar nuestro modelo de vecinos más cercanos (KNN) en el conjunto de datos objeto de análisis. Esto proporciona una forma conveniente de encapsular todo el flujo de trabajo de modelado en una sola entidad.

pipeline_knn = Pipeline(steps_knn)

Transformacion de los datos y definición del rango para el valor de k

x_train_knn = pipeline_knn.fit_transform(x_train_knn)
k_values = np.linspace(1, 10, 10, dtype='int64')

A continuación, identificados por medio de una curva de validación cual es el mejor k para evaluar que tan bien el modelo predice, La función validation_curve realizará la validación cruzada para cada valor en param_range y devolvuelve las puntuaciones de entrenamiento y validación para cada valor de parámetro. Estas puntuaciones se pueden utilizar para analizar cómo varía el rendimiento del modelo con diferentes valores de hiperparámetro y seleccionar el mejor valor para optimizar el rendimiento del modelo.

train_scores_knn, val_scores_knn = validation_curve(estimator=KNeighborsClassifier(),
                                            X=x_train_knn,
                                            y=y_train_knn,
                                            param_name='n_neighbors',
                                            param_range=k_values,
                                            scoring='accuracy',
                                            cv=10)

Se extraen las metricas descriptivas

train_mean_knn = np.mean(train_scores_knn, axis=1)
train_std_knn = np.std(train_scores_knn, axis=1)
val_mean_knn = np.mean(val_scores_knn, axis=1)
val_std_knn = np.std(val_scores_knn, axis=1)
import plotly.graph_objects as go
fig = go.Figure()


fig.add_trace(go.Scatter(x=k_values, y=train_mean_knn+train_std_knn,mode='lines',line_color="red",showlegend=False))
fig.add_trace(go.Scatter(x=k_values, y=train_mean_knn,mode='lines+markers',name='Exactitud Entrenamiento',line_color="red",fill='tonexty'))
fig.add_trace(go.Scatter(x=k_values, y=train_mean_knn-train_std_knn,mode='lines',line_color="red",fill='tonexty',showlegend=False))

fig.add_trace(go.Scatter(x=k_values, y=val_mean_knn+train_std_knn,mode='lines',line_color="green",showlegend=False))
fig.add_trace(go.Scatter(x=k_values, y=val_mean_knn,mode='lines+markers',name='Exactitud Test',line_color="green",fill='tonexty'))
fig.add_trace(go.Scatter(x=k_values, y=val_mean_knn-train_std_knn,mode='lines',line_color="green",fill='tonexty',showlegend=False))

fig.update_layout(title="Exactitud modelo",xaxis_title="K",yaxis_title="Exactitud",legend=dict(yanchor="bottom",xanchor="right",y=0.1))
fig.show()
mejor_indice_knn = np.argmax(val_mean_knn)
mejor_k = k_values[mejor_indice_knn]
print(mejor_k)
7

una vez identificamos el mejor k, en este caso \(k = 7\), entrenamos nuevammente el modelo.

mejor_modelo_knn = KNeighborsClassifier(n_neighbors=mejor_k).fit(x_train_knn, y_train_knn)
x_test_knn_trans = pipeline_knn.fit_transform(x_test_knn)

A continuación se realiza la predicción del modelo

y_pred_knn = mejor_modelo_knn.predict(x_test_knn_trans)

Análisis de residuales.#

# Calcular los residuos
ei_knn = y_test_knn - y_pred_knn

#Prueba de normalidad de Kolmogorov-Smirnov
from scipy.stats import kstest

ks_result_knn = kstest(ei_knn, 'norm')
print("Prueba de Kolmogorov-Smirnov:")
print("Estadístico:", ks_result_knn.statistic)
print("Valor p:", ks_result_knn.pvalue)
Prueba de Kolmogorov-Smirnov:
Estadístico: 0.4033333333333333
Valor p: 1.3805212034444417e-44

Dado que el P-value \(=1.3805212034444417e-44 < 0.05\) hay evidencia significativa para rechazar la hipótesis nula con una significancia \(\alpha = 0.05\), es decir que los residuos no siguen una distribución.normal

import statsmodels.api as sm

# Prueba de Durbin-Watson
durbin_watson_statistic = sm.stats.stattools.durbin_watson(ei_knn)
print("Estadístico de Durbin-Watson:", durbin_watson_statistic)
Estadístico de Durbin-Watson: 2.1222222222222222

Dado que el estadístico de Durbin_Watson \( = 2.12\), se puede afirmar que no existe una correlacion serial en los residuales.

import statsmodels.graphics.tsaplots as tsaplots


# Gráfico QQ-plot
fig_error2, axs = plt.subplots(1, 1, figsize=(15, 5))

# QQ-plot para los residuales
stats.probplot(ei_knn, dist="norm", plot=axs)
axs.get_lines()[0].set_color('#008080')  # Establecer el color del gráfico
axs.get_lines()[0].set_markersize(2)     # Establecer el tamaño de los puntos
axs.set_title('QQ-plot para los residuales ordinarios', fontsize=16)
axs.tick_params(axis='x', labelsize=10)  # Solo para el eje x (inferior)
axs.tick_params(axis='y', labelsize=10)  # Solo para el eje y (izquierdo)

# Ajustar el espaciado entre los gráficos
plt.tight_layout()

# Mostrar la figura
plt.show()
_images/b79e79bb07546f5ddfb1f8182f02138a5815d54494f1e58d877064a998ba1830.png

Basándonos en el gráfico anterior, podemos concluir que, si bien los residuos no siguen estrictamente una distribución normal, sí se observa que cumplen con el supuesto de independencia. Esto se refiere a que los residuos no muestran patrones discernibles en su distribución, lo que sugiere que están aleatoriamente dispersos.

Métricas de evaluación de modelos#

Se genera la matriz de confusión

cm_knn = confusion_matrix(y_test_knn, y_pred_knn, labels=mejor_modelo_knn.classes_)
disp = ConfusionMatrixDisplay(confusion_matrix=cm_knn,display_labels=mejor_modelo_knn.classes_)
disp.plot(cmap=plt.cm.RdPu)
<sklearn.metrics._plot.confusion_matrix.ConfusionMatrixDisplay at 0x1e448ec94c0>
_images/fdf796f43a4e82020572af95a450b705b50202a6ba893346f7ed33b1d10df41a.png

Según el análisis del gráfico, observamos el siguiente comportamiento del modelo de regresión logístico (Clasificación):

  • De un total de 300 predicciones realizadas, 185 se clasificaron correctamente como verdaderas positivas. Esto significa que se identificaron adecuadamente los clientes considerados como buenos pagadores, quienes en realidad lo son.

  • Asimismo, de las 300 predicciones, 25 fueron clasificadas correctamente como verdaderas negativas. Esto indica que se identificaron correctamente los clientes catalogados como malos pagadores, quienes realmente lo son.

  • Sin embargo, se presentaron 29 falsos negativos entre las predicciones. Esto implica que algunos clientes que en realidad son buenos pagadores fueron erróneamente clasificados como malos pagadores.

  • Por otro lado, se registraron 61 falsos positivos. Este resultado indica que algunos clientes que son malos pagadores fueron incorrectamente clasificados como buenos pagadores.

Con el siguiente codigo generamos las metricas de evaluación

accuracy_knn = accuracy_score(y_test_knn, y_pred_knn)

precision_knn_0 = precision_score(y_test_knn, y_pred_knn,pos_label=0)
precision_knn_1 = precision_score(y_test_knn, y_pred_knn,pos_label=1)

recall_knn_0 = recall_score(y_test_knn, y_pred_knn,pos_label=0)
recall_knn_1 = recall_score(y_test_knn, y_pred_knn,pos_label=1)

f1_knn_0 = f1_score(y_test_knn, y_pred_knn,pos_label=0)
f1_knn_1 = f1_score(y_test_knn, y_pred_knn,pos_label=1)


dato_comparativo_knn = {
                '|':['|','|'],
                'Modelo':['K-Vecinos',''],
               'Clase':['No','Si'],
               'Accuracy':[accuracy_knn,accuracy_knn],
               'Precision':[precision_knn_0,precision_knn_1],
                'Recall':[recall_knn_0,recall_knn_1],
                'F1 Score':[f1_knn_0,f1_knn_1]}

y_prob_pred_knn = mejor_modelo_knn.predict_proba(x_test_knn_trans)[:, 1] 

knn_fpr, knn_tpr, thresholds = sklearn.metrics.roc_curve(y_test_knn, y_prob_pred_knn) 

resultado_knn= pd.DataFrame(dato_comparativo_knn)
print(resultado_knn)
   |     Modelo Clase  Accuracy  Precision    Recall  F1 Score
0  |  K-Vecinos    No       0.7   0.462963  0.290698  0.357143
1  |               Si       0.7   0.752033  0.864486  0.804348

De lo anterior, podemos inferir:

  • Accuracy (Precisión): Ambos resultados tienen una precisión del 70%, lo que significa que el 70% de las predicciones son correctas en general.

  • Precision (Precisión):

    • Para la clase “No” (mal pagador), la precisión es del 46.3%, lo que indica que el 46.3% de las predicciones positivas para esta clase son correctas.

    • Para la clase “Sí” (buen pagador), la precisión es del 75.2%, lo que significa que el 75.2% de las predicciones positivas para esta clase son correctas.

  • Recall (Recuperación):

    • Para la clase “No” (mal pagador), el valor de recuperación es del 29.1%, lo que sugiere que el 29.1% de los verdaderos casos positivos fueron identificados correctamente.

    • Para la clase “Sí” (buen pagador), el valor de recuperación es del 86.4%, indicando que el 86.4% de los verdaderos casos positivos fueron identificados correctamente.

  • F1 Score:

    • Para la clase “No” (mal pagador), el puntaje F1 es del 35.7%, que es la media armónica entre precisión y recuperación, proporcionando una medida balanceada entre ambas métricas.

    • Para la clase “Sí” (buen pagador), el puntaje F1 es del 80.4%.

En resumen, aunque la precisión general es la misma para ambas clases, hay diferencias significativas en la precisión, recuperación y puntuación F1 entre las clases “No” y “Sí”. Esto sugiere que el modelo puede estar mejorando en la predicción de buenos pagadores en comparación con los malos pagadores. es.

A continuación se visualiza la curva ROC.

# Calcular el AUC
auc_value_knn = auc(knn_fpr, knn_tpr)

# Crear la figura
fig_roc2 = go.Figure()

# Agregar el área bajo la curva ROC
fig_roc2.add_trace(go.Scatter(
    x=knn_fpr, y=knn_tpr,
    fill='tozeroy',
    mode='lines',
    line=dict(color='rgba(128,0,128,0.2)'),
    name=f'ROC Curve (AUC={auc_value_knn:.4f})'
))

# Agregar la línea de referencia diagonal
fig_roc2.add_shape(
    type='line', line=dict(dash='dash'),
    x0=0, x1=1, y0=0, y1=1
)

# Agregar etiqueta con el valor del AUC
fig_roc2.add_annotation(
    xref="paper", yref="paper",  # Coordenadas relativas al gráfico
    x=0.95, y=0.05,  # Coordenadas para la esquina inferior derecha
    text=f'AUC: {auc_value_knn:.4f}',
    showarrow=False,
    font=dict(size=12, color='black'),  # Estilo del texto
    bordercolor='black',  # Color del borde del rectángulo
    borderwidth=1,  # Ancho del borde del rectángulo
    bgcolor='rgba(255,255,255,0.7)'  # Color de fondo del rectángulo con opacidad
)

# Actualizar las etiquetas y título
fig_roc2.update_layout(
    title='ROC Curve',
    xaxis_title='False Positive Rate',
    yaxis_title='True Positive Rate',
    width=700, height=500
)

fig_roc2.show()

Con un valor de 0,6868 para el área bajo la curva ROC (AUC), podemos afirmar que nuestro modelo tiene un buen rendimiento en términos de su capacidad para discriminar entre las clases positivas y negativas. Cuanto más cercano a 1 sea el valor de AUC, mejor será la capacidad predictiva del modelo, y un valor de 0,6868 indica una capacidad de discriminación bueno.

Conclusiones#

Al comparar los resultados entre el modelo de regresión logística y el modelo KNN (Vecinos más cercanos), destacan las siguientes diferencias:

Ambos modelos muestran una capacidad similar para identificar verdaderos positivos y verdaderos negativos, lo que sugiere una habilidad adecuada para discernir entre buenos y malos pagadores.

No obstante, el modelo de regresión logística exhibe una menor incidencia de falsos negativos y falsos positivos en contraste con el modelo KNN. Esta disparidad indica una mayor precisión en la clasificación de buenos y malos pagadores por parte del modelo de regresión logística.

En términos generales, el modelo de regresión logística presenta un desempeño superior en la tarea de clasificación. Su mayor precisión en la identificación de ambas clases y su menor tasa de errores de clasificación en comparación con el modelo KNN respaldan esta afirmación.

Al analizar los valores obtenidos para el área bajo la curva ROC (AUC), observamos un valor de 0.7741 para el modelo Logit y 0.6868 para el KNN. Esta discrepancia indica que el modelo Logit supera al KNN en términos de su capacidad para discriminar entre las clases positivas y negativas.

En la práctica, esto implica que el modelo Logit es más efectivo para realizar predicciones precisas sobre las clases de interés en el conjunto de datos evaluado, en comparación con el modelo KNN. Por lo tanto, si se busca el modelo con una mejor capacidad predictiva, el modelo Logit sería la opción preferida en este caso.